import fs from 'fs'
import parser from '@babel/parser'
import _traverse from '@babel/traverse'
const traverse = _traverse.default || _traverse
import t from '@babel/types'
import { generate } from '@babel/generator'

const code = fs.readFileSync('./input.js', { encoding: 'utf-8' })
const ast = parser.parse(code, {
  sourceType: 'module'
})

function base64Encode(str) {
  return Buffer.from(str).toString('base64')
}

function hexEncode(str) {
  let hexStr = ''
  for (let i = 0; i < str.length; i++) {
    hexStr += '\\x' + str.charCodeAt(i).toString(16)
  }
  return hexStr
}

let bigArr = []

const MemberExpression = path => {
  path.node.computed = true
  path.node.property = t.stringLiteral(path.node.property.name)
}

const Identifier = path => {
  if (
    [
      'eval',
      'parseInt',
      'encodeURIComponent',
      'Object',
      'Function',
      'Boolean',
      'Number',
      'Date',
      'Math',
      'String',
      'RegExp',
      'Array'
    ].includes(path.node.name)
  ) {
    path.replaceWith(
      t.memberExpression(t.identifier('window'), t.identifier(path.node.name))
    )
  }
}

const NumberLiteral = path => {
  let value = path.node.value
  let key = parseInt(Math.random() * (999999 - 100000) + 100000, 10)
  let cipherNum = value ^ key
  path.replaceWith(
    t.binaryExpression('^', t.numericLiteral(cipherNum), t.numericLiteral(key))
  )
  path.skip()
}

const StringLiteral = path => {
  let cipherText = base64Encode(path.node.value)
  let bigArrayIndex = bigArr.indexOf(cipherText)
  let index = bigArrayIndex
  if (index === -1) {
    index = bigArr.push(cipherText) - 1
  }

  let encStr = t.callExpression(t.identifier('atob'), [
    t.memberExpression(t.identifier('arr'), t.numericLiteral(index), true)
  ])
  path.replaceWith(encStr)
  path.skip()
}

const genId = num => {
  let flag = ['O', 'o', '0']
  let retval = []
  while (num > 0) {
    retval.push(flag[num % 3])
    num = parseInt(num / 3, 10)
  }

  retval[0] === '0' && (retval[0] = 'O')
  return retval.join('')
}

const renameOwnBinding = path => {
  let OwnBindingObj = {}
  let globalBindingObj = {}
  let i = 0
  path.traverse({
    Identifier(p) {
      let name = p.node.name
      let binding = p.scope.getOwnBinding(name)

      binding ? (OwnBindingObj[name] = binding) : (globalBindingObj[name] = 1)
      for (let oldName in OwnBindingObj) {
        do {
          var newName = genId(i++)
        } while (globalBindingObj[newName])
        OwnBindingObj[oldName].scope.rename(oldName, newName)
      }
    }
  })
}

const BinaryExpression = path => {
  let left = path.node.left
  let right = path.node.right
  let operator = path.node.operator
  let a = t.identifier('a')
  let b = t.identifier('b')
  let funcNameIdentifier = path.scope.generateUidIdentifier('xxx')
  let func = t.functionDeclaration(
    funcNameIdentifier,
    [a, b],
    t.blockStatement([
      t.returnStatement(
        t.binaryExpression(operator, t.identifier(a.name), t.identifier(b.name))
      )
    ])
  )
  let BlockStatement = path.findParent(p => p.isBlockStatement())
  BlockStatement.node.body.unshift(func)
  path.replaceWith(
    t.callExpression(t.identifier(funcNameIdentifier.name), [left, right])
  )
}

const FunctionExpression = path => {
  let blockStatement = path.node.body

  let Statements = blockStatement.body.map(i => {
    if (t.isReturnStatement(i)) {
      return i
    }
    if (!(i.leadingComments && i.leadingComments[0].value.includes('eval'))) {
      return i
    }

    delete i.leadingComments

    let code = generate(i).code

    let codeAscii = Array.from(code).map(char =>
      t.numericLiteral(char.charCodeAt(0))
    )
    let decryptFuncName = t.memberExpression(
      t.identifier('String'),
      t.identifier('fromCodePoint')
    )

    let decryptFunc = t.callExpression(decryptFuncName, codeAscii)

    return t.expressionStatement(
      t.callExpression(t.identifier('eval'), [decryptFunc])
    )
  })
  path.get('body').replaceWith(t.blockStatement(Statements))
}

const FunctionExpression2 = path => {
  let blockStatement = path.node.body

  let Statements = blockStatement.body.map((item, index) => {
    return {
      index,
      value: item
    }
  })

  let i = Statements.length

  while (i) {
    let j = Math.floor(Math.random() * i--)
    ;[Statements[i], Statements[j]] = [Statements[j], Statements[i]]
  }

  let dispenserArr = []
  let cases = []
  Statements.map((v, i) => {
    dispenserArr[v.index] = i
    cases.push(
      t.switchCase(t.numericLiteral(i), [v.value, t.continueStatement()])
    )
  })

  let dispenserStr = dispenserArr.join('|')
  let array = path.scope.generateUidIdentifier('array')
  let index = path.scope.generateUidIdentifier('index')
  let callee = t.memberExpression(
    t.callExpression(t.identifier('atob'), [
      t.stringLiteral(base64Encode(dispenserStr))
    ]),
    t.stringLiteral('split'),
    true
  )

  let arrayInit = t.callExpression(callee, [t.stringLiteral('|')])

  let varArray = t.variableDeclarator(array, arrayInit)
  let varIndex = t.variableDeclarator(index, t.numericLiteral(0))

  let dispenser = t.variableDeclaration('let', [varArray, varIndex])

  let updExp = t.updateExpression('++', index)
  let memExp = t.memberExpression(array, updExp, true)
  let discriminant = t.unaryExpression('+', memExp)

  let switchSta = t.switchStatement(discriminant, cases)
  let unaExp = t.unaryExpression('!', t.arrayExpression([]))
  unaExp = t.unaryExpression('!', unaExp)
  let whileSta = t.whileStatement(
    unaExp,
    t.blockStatement([switchSta, t.breakStatement()])
  )

  const VariableDeclaration = path => {
    path.node.kind = 'var'
  }

  path.get('body').replaceWith(t.blockStatement([dispenser, whileSta]))
  path.skip()

  path.traverse({
    VariableDeclaration,
    NumberLiteral
  })
}

const FunctionExpression3 = path => {
  let blockStatement = path.node.body
  if (blockStatement.body.length < 2) return

  path.traverse({
    VariableDeclaration(p) {
      const declarations = p.node.declarations
      let statements = []
      declarations.map(i => {
        path.node.params.push(i.id)
        i.init && statements.push(t.assignmentExpression('=', i.id, i.init))
      })
      p.replaceInline(statements)
    }
  })

  let firstSta = blockStatement.body[0]
  let i = 1
  while (i < blockStatement.body.length) {
    let tempSta = blockStatement.body[i++]
    const secondSta = t.isExpressionStatement(tempSta)
      ? tempSta.expression
      : tempSta

    if (t.isReturnStatement(secondSta)) {
      firstSta = t.returnStatement(
        t.toSequenceExpression([firstSta, secondSta.argument])
      )
    } else if (t.isAssignmentExpression(secondSta)) {
      if (t.isCallExpression(secondSta.right)) {
        let callee = secondSta.right.callee
        callee.object = t.toSequenceExpression([firstSta, callee.object])
        firstSta = secondSta
      } else {
        secondSta.right = t.toSequenceExpression([firstSta, secondSta.right])
        firstSta = secondSta
      }
    } else {
      firstSta = t.toSequenceExpression([firstSta, secondSta])
    }
  }
  path.get('body').replaceWith(t.blockStatement([firstSta]))
}

traverse(ast, {
  // a[b]
  MemberExpression,
  // window.a
  Identifier,
  // 1 ^ 2
  NumberLiteral,
  // atob
  StringLiteral,
  // 花指令
  BinaryExpression,
  'Program|FunctionDeclaration|FunctionExpression'(path) {
    // OoOoOo
    renameOwnBinding(path)
    if (path.isFunctionExpression()) {
      // eval
      FunctionExpression(path)
      // ,
      FunctionExpression3(path)
    }
  }
})

let astArrDecode = parser.parse(`
    (function unshuffle(arr) {
        for (let i = arr.length; i > 0; i--) {
            arr.push(arr.shift())
            i--
        }
    })(arr)
`)

traverse(astArrDecode, {
  NumberLiteral,
  MemberExpression(path) {
    if (t.isIdentifier(path.node.property)) {
      path.node.property = t.stringLiteral(hexEncode(path.node.property.name))
    }
    path.node.computed = true
  }
})

ast.program.body.unshift(astArrDecode.program.body[0])

function shuffle(arr) {
  for (let i = arr.length; i > 0; i--) {
    arr.unshift(arr.pop())
    i--
  }
  return arr
}

bigArr = shuffle(bigArr)
bigArr = bigArr.map(i => t.stringLiteral(i))
bigArr = t.variableDeclarator(t.identifier('arr'), t.arrayExpression(bigArr))
bigArr = t.variableDeclaration('var', [bigArr])
ast.program.body.unshift(bigArr)

const outputCode = generate(ast, {
  comments: false
}).code
const output = outputCode.replace(/\\\\x/g, '\\x')

const output2 = parser.parse(output, {
  sourceType: 'module'
})
traverse(output2, {
  // case
  FunctionExpression(path) {
    FunctionExpression2(path)
  }
})

const importDeclaration = t.importDeclaration(
  [t.importDefaultSpecifier(t.identifier('CryptoJS'))],
  t.stringLiteral('crypto-js')
)

output2.program.body.unshift(importDeclaration)

const outputCode2 = generate(output2).code

fs.writeFileSync('./output.js', outputCode2, { encoding: 'utf-8' })

console.log('代码转换完成，输出文件为 output.js')
